package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
)

//创建go模块
//go mod init gofirst
//添加缺少的模块并移除不需要的模块
//go mod tidy

//按下面链接操作，解决点击Run and Debug无法调试的问题
//https://zhuanlan.zhihu.com/p/517836734

//ioutil包的函数已经被移除，使用os包的函数替代

// 定义一个全局变量，用于标识是否只查找
var searchOnly bool = false

// 文件列表，保存所有可以转换的文件路径
var fileList []string

// 分隔符
var splitChar string = "@"

func main() {
	//打印使用帮助
	if len(os.Args) == 1 {
		fmt.Println("GBK to UTF-8 BOM Converter")
		fmt.Println("Usage:")
		fmt.Println("GBK2UTF8 <path> [-S] [-T]")
		fmt.Println("    <path> 指定要搜索的路径")
		fmt.Println("    -S     只搜索，不转换")
		fmt.Println("    -T     日志显示文件名和行号")
		return
	}

	// build with: go build

	//package command-line-arguments is not a main package
	//go run main.go

	//打开日志文件
	logFile, err := os.OpenFile("gbk2utf8_log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal(err)
		return
	}
	//关闭日志文件
	defer logFile.Close()
	//设置日志输出格式
	logflag := log.Ldate | log.Ltime
	log.SetFlags(logflag)
	//设置日志输出到文件
	log.SetOutput(logFile)

	//按传入的第一个参数，搜索指定路径下所有文件，包括子目录
	if len(os.Args) > 1 {
		//第二个参数为-S时，只将匹配的文件名记录到文件
		if len(os.Args) > 2 && os.Args[2] == "-S" {
			//设置标识为只查找
			searchOnly = true
		}
		if len(os.Args) > 2 && os.Args[2] == "-T" {
			logflag = logflag | log.Lshortfile
			log.SetFlags(logflag)
		}
		//GBK to UTF-8转换流程
		searchFile(os.Args[1])
		if !searchOnly {
			processFileList()
			checkFileList()
		} else {
			//只查找，结果输出到日志文件
			for _, fileName := range fileList {
				log.Print(fileName)
			}
		}
	}
}

// fileList中的内容写入文件
// func writeToFile(fileName string) {
// 	//fileList为空时，不写入文件
// 	if len(fileList) == 0 {
// 		return
// 	}
// 	//fileList内容写入buffer
// 	var buffer bytes.Buffer
// 	for _, fileName := range fileList {
// 		buffer.WriteString(fileName + "\r\n")
// 	}
// 	//将buffer内容写入文件
// 	err := os.WriteFile(fileName, buffer.Bytes(), 0666)
// 	if err != nil {
// 		log.Println(err)
// 		return
// 	}
// }

// 检查文件列表中文件是否按编码转换
func checkFileList() {
	for _, fileName := range fileList {
		//将文件名按:分割
		fileNameArray := strings.Split(fileName, splitChar)
		//检查文件是否带BOM
		var done = hasBOM(fileNameArray[1])
		var donestr = "Done: "
		if !done {
			donestr = "Error: "
		}
		log.Print(donestr + fileName)

		// if !hasBOM(fileNameArray[1]) {
		// 	//将文件名写入文件
		// 	writeErrorFile(fileName)
		// }
	}
}

// 处理文件列表
func processFileList() {
	for _, fileName := range fileList {
		//将文件名按分割符分割
		fileNameArray := strings.Split(fileName, splitChar)
		//如果文件编码为GBK，则转换为UTF-8
		if fileNameArray[0] == "GBK" {
			writeUtf8WithBOM(fileNameArray[1])
		}
		//如果文件编码为UTF-8，只添加BOM
		if fileNameArray[0] == "UTF-8" {
			//写入BOM
			writeBOM(fileNameArray[1])
		}
	}
}

// 搜索指定路径下所有文件，包括子目录
func searchFile(path string) {
	//打开文件夹
	dir, err := os.Open(path)
	if err != nil {
		log.Println(err)
		return
	}
	defer dir.Close()
	//读取文件列表
	fileInfos, err := dir.Readdir(-1)
	if err != nil {
		log.Println(err)
		return
	}
	//遍历文件列表
	for _, fi := range fileInfos {
		//判断是否是目录，如果是则继续搜索
		if fi.IsDir() {
			searchFile(path + "/" + fi.Name())
		} else {
			// fmt.Println(path + "/" + fi.Name())
			//判断文件的后缀是否为.h或者.c
			if isSourceFile(fi.Name()) {
				//编写函数，实现检测打开文件的编码
				encoding, err := detectEncoding(path + "/" + fi.Name())
				if err != nil {
					log.Println(err)
					continue
				}
				//如果文件编码为UTF-8
				if encoding == "UTF-8" {
					//添加到文件列表，格式为encoding:文件名
					addToFileList(encoding, path, fi.Name())
				}
				// fmt.Println(encoding)
				//如果文件编码为GBK，则转换为UTF-8
				if encoding == "GBK" {
					//添加到文件列表，格式为encoding:文件名
					addToFileList(encoding, path, fi.Name())
				}
			}
		}
	}
}

// 添加到文件列表，格式为encoding@文件名
func addToFileList(encoding string, path string, fileName string) {
	fileList = append(fileList, encoding+splitChar+path+"/"+fileName)
}

// 将出错的文件名写入文件
// func writeErrorFile(fileName string) {
// 	//打开文件
// 	file, err := os.OpenFile("errorlist.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// 	if err != nil {
// 		log.Println(err)
// 		return
// 	}
// 	defer file.Close()
// 	//写入文件名
// 	_, err = file.WriteString(fileName + "\r\n")
// 	if err != nil {
// 		log.Println(err)
// 		return
// 	}
// }

// 将文件编码改为带BOM的UTF-8，原文件不保留
func writeUtf8WithBOM(fileName string) {

	//以只读方式打开文件
	file, err := os.OpenFile(fileName, os.O_RDWR, 0666)
	if err != nil {
		log.Println(err)
		return
	}
	// defer file.Close()
	buffer, err := io.ReadAll(file)
	if err != nil {
		log.Println(err)
		return
	}
	//等待文件完全关闭
	err = file.Sync()
	if err != nil {
		log.Println(err)
		return
	}
	//关闭文件
	err = file.Close()
	if err != nil {
		log.Println(err)
		return
	}

	// gbkstr := string(buffer)
	// fmt.Println(gbkstr)

	//buffer转换为UTF-8
	buffer, err = io.ReadAll(transform.NewReader(bytes.NewReader(buffer), simplifiedchinese.GBK.NewDecoder()))
	if err != nil {
		log.Println(err)
		return
	}
	//buffer to string
	// utf8str := string(buffer)
	// fmt.Println(utf8str)
	//在buffer前加BOM
	buffer = append([]byte{0xEF, 0xBB, 0xBF}, buffer...)
	//将文件内容写入到临时文件
	tempFileName := fileName + ".tmp"
	err = os.WriteFile(tempFileName, buffer, 0666)
	if err != nil {
		log.Println(err)
		return
	}
	//删除原文件
	err = os.Remove(fileName)
	if err != nil {
		log.Println(err)
		return
	}
	//将临时文件重命名为原文件
	err = os.Rename(tempFileName, fileName)
	if err != nil {
		log.Println(err)
		return
	}
}

// 判断文件的后缀是否为.h或者.cpp
func isSourceFile(fileName string) bool {
	//判断文件后缀
	if strings.HasSuffix(fileName, ".h") || strings.HasSuffix(fileName, ".cpp") {
		return true
	}
	return false
}

// 检测文件的编码，空文件返回Unknown
func detectEncoding(fileName string) (string, error) {
	//打开文件
	file, err := os.Open(fileName)
	if err != nil {
		return "", err
	}
	defer file.Close()
	//获得文件大小
	fileInfo, err := file.Stat()
	if err != nil {
		return "", err
	}
	//文件大小
	fileSize := fileInfo.Size()
	//如果文件大小为0，则返回Unknown
	if fileSize == 0 {
		return "Unknown", nil
	}
	//只取512个字节，出现漏检，按全文件检测，保证正确性
	//如果文件大小大于512个字节，则读取前512个字节
	// if fileSize > 512 {
	// 	fileSize = 512
	// }
	buffer := make([]byte, fileSize)
	_, err = file.Read(buffer)
	if err != nil {
		return "", err
	}
	//检测编码
	return detectEncodingByBuffer(buffer), nil
}

// 实现detectEncodingByBuffer
func detectEncodingByBuffer(buffer []byte) string {
	//检测是否是UTF-8BOM编码
	if len(buffer) > 2 {
		//检测文件前3个字节是否为BOM
		if buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF {
			return "UTF-8BOM"
		}
	}
	//检测是否是UTF-8编码
	//utf8包的Valid函数检测UTF-8编码结果不准确，所以不使用
	// if utf8.Valid(buffer) {
	if validUTF8(buffer) {
		return "UTF-8"
	}
	//检测是否是UTF-16编码
	if len(buffer) > 1 && buffer[0] == 0xFE && buffer[1] == 0xFF {
		return "UTF-16BE"
	}
	if len(buffer) > 1 && buffer[0] == 0xFF && buffer[1] == 0xFE {
		return "UTF-16LE"
	}
	//检测是否是UTF-32编码
	if len(buffer) > 3 && buffer[0] == 0x00 && buffer[1] == 0x00 && buffer[2] == 0xFE && buffer[3] == 0xFF {
		return "UTF-32BE"
	}
	if len(buffer) > 3 && buffer[0] == 0xFF && buffer[1] == 0xFE && buffer[2] == 0x00 && buffer[3] == 0x00 {
		return "UTF-32LE"
	}
	//遍历buffer
	for i := 0; i < len(buffer)-1; i++ {
		//检测是否是GBK编码
		if buffer[i] >= 0x81 && buffer[i] <= 0xFE && buffer[i+1] >= 0x40 && buffer[i+1] <= 0xFE {
			return "GBK"
		}
		//检测是否是Big5编码
		if (buffer[i] >= 0xA4 && buffer[i] <= 0xC6 || buffer[i] >= 0xC9 && buffer[i] <= 0xF9) && buffer[i+1] >= 0x40 && buffer[i+1] <= 0x7E {
			return "Big5"
		}
	}
	return "Unknown"
}

// 检测文件是否带BOM
func hasBOM(fileName string) bool {
	//打开文件
	file, err := os.Open(fileName)
	if err != nil {
		log.Println(err)
		return false
	}
	defer file.Close()
	//读取文件前3个字节
	buffer := make([]byte, 3)
	_, err = file.Read(buffer)
	if err != nil {
		log.Println(err)
		return false
	}
	//检测文件前3个字节是否为BOM
	if buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF {
		return true
	}
	return false
}

// 将BOM写入文件
func writeBOM(fileName string) {

	//以只读方式打开文件
	file, err := os.OpenFile(fileName, os.O_RDWR, 0666)
	if err != nil {
		log.Println(err)
		return
	}
	// defer file.Close()
	buffer, err := io.ReadAll(file)
	if err != nil {
		log.Println(err)
		return
	}
	//等待文件完全关闭
	err = file.Sync()
	if err != nil {
		log.Println(err)
		return
	}
	//关闭文件
	err = file.Close()
	if err != nil {
		log.Println(err)
		return
	}
	//在buffer前加BOM
	buffer = append([]byte{0xEF, 0xBB, 0xBF}, buffer...)
	//将文件内容写入到临时文件
	tempFileName := fileName + ".tmp"
	err = os.WriteFile(tempFileName, buffer, 0666)
	if err != nil {
		log.Println(err)
		return
	}
	//删除原文件
	err = os.Remove(fileName)
	if err != nil {
		log.Println(err)
		return
	}
	//将临时文件重命名为原文件
	err = os.Rename(tempFileName, fileName)
	if err != nil {
		log.Println(err)
		return
	}
}

// 检测是否是UTF-8编码，找到第一组特征值即返回true
// UTF-8编码特征值：首字节高位到低位有几个1，即为该字符占几个字节(包括首字节，最少占2个字节，最多占5个字节)，后续字节均以10开头
func validUTF8(buf []byte) bool {
	found := false
	nBytes := 0
	for i := 0; i < len(buf); i++ {
		if nBytes == 0 {
			if (buf[i] & 0x80) != 0 { //与操作之后不为0，说明首位为1
				for (buf[i] & 0x80) != 0 {
					buf[i] <<= 1 //左移一位
					nBytes++     //记录字符共占几个字节
				}

				if nBytes < 2 || nBytes > 6 { //因为UTF8编码单字符最多不超过6个字节
					return false
				}
				//找到utf8字符的首字节
				found = true

				nBytes-- //减掉首字节的一个计数
			}
		} else { //处理多字节字符
			if buf[i]&0xc0 != 0x80 { //判断多字节后面的字节是否是10开头
				found = false
				return false
			}
			nBytes--
			if found && nBytes == 0 {
				//找到utf8字符的尾字节
				return true
			}
		}
	}
	return false
}
